package avicit.bdp.dgs.qa.service;

import avicit.bdp.common.base.BaseService;
import avicit.bdp.common.dto.ProcessInstanceDto;
import avicit.bdp.common.service.dao.BdpCommonDao;
import avicit.bdp.common.utils.BdpLogUtil;
import avicit.bdp.core.constant.Constants;
import avicit.bdp.dds.dispatch.model.ThreeTuple;
import avicit.bdp.dgs.qa.dao.JobReportDAO;
import avicit.bdp.dgs.qa.dto.*;
import avicit.bdp.dgs.qa.utils.constant.QaConstant;
import avicit.bdp.dgs.qa.utils.enums.*;
import avicit.bdp.dgs.qa.utils.redis.RedisUtils;
import avicit.bdp.dgs.qa.utils.tools.DateUtils;
import avicit.platform6.api.sysmsg.dto.SysMsgDTO;
import avicit.platform6.api.system.SysMsgClient;
import avicit.platform6.commons.utils.ComUtil;
import avicit.platform6.commons.utils.JsonHelper;
import avicit.platform6.core.context.ThreadContextHelper;
import avicit.platform6.core.exception.BusinessException;
import avicit.platform6.core.rest.msg.ResponseMsg;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.core.type.TypeReference;
import com.github.pagehelper.Page;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.googlecode.aviator.AviatorEvaluator;
import org.apache.commons.codec.CharEncoding;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xddf.usermodel.chart.XDDFChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFDataSource;
import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory;
import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.xwpf.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

/**
 * @金航数码科技有限责任公司
 * @作者：developer
 * @邮箱：developer@avic-digital.com
 * @创建时间： 2023-07-18
 * @类说明：JobReportService
 * @修改记录：
 * @注意事项：
 * @主要功能：质量报告Service
 */
@Service
public class JobReportService extends BaseService<JobReportDAO, JobReportDTO> {
    private static final Logger logger = LoggerFactory.getLogger(JobReportService.class);

    private static final String REPORT_TMEPLATE_NAME = "数据质量报告模板V1.0.docx";
    private static final String EMPTY_REPORT_TMEPLATE_NAME = "数据质量空报告模板V1.0.docx";
    private static final String FILE_REPORT_TMEPLATE_NAME = "文件校验质量报告模板V1.0.docx";
    private static final String FILE_EMPTY_REPORT_TMEPLATE_NAME = "文件校验质量空报告模板V1.0.docx";

    private static final int HOUR_MILLS = 60 * 60 * 1000;

    /**
     * 数据概览-质量评分分布图使用
     */
    private static List<ThreeTuple> scoreGroup = new ArrayList<>();

    static {
        scoreGroup.add(new ThreeTuple<>("20以下", null, 20));
        scoreGroup.add(new ThreeTuple<>("20-40", 20, 40));
        scoreGroup.add(new ThreeTuple<>("40-60", 40, 60));
        scoreGroup.add(new ThreeTuple<>("60-80", 60, 80));
        scoreGroup.add(new ThreeTuple<>("80-100", 80, null));
    }

    /**
     * 生成报告的线程池
     */
    private ExecutorService executorService;

    @Resource
    private BdpCommonDao commonDao;

    @Autowired
    private JobResultService jobResultService;

    @Autowired
    private SubscribeService subscribeService;

    @Autowired
    private SysMsgClient sysMsgClient;

    @Resource
    RedisUtils redisUtils;

    /**
     * 删除/批量删除质量作业，多个id使用英文逗号隔开
     *
     * @param jobId
     * @param insIdList
     * @return
     */
    public int deleteJobReport(String jobId, List<String> insIdList) {
        if (StringUtils.isBlank(jobId) && CollectionUtils.isEmpty(insIdList)) {
            logger.warn("质量作业ID和流程实例ID不能同时为空");
            return 0;
        }

        return this.mapper.deleteJobReportByCond(jobId, insIdList);
    }

    /**
     * 查看质量报告
     *
     * @param processInstanceId 根据流程实例ID查询历史任务执行结果
     * @param jobDTO            质量作业详情
     * @return
     */
    public JobReportDTO queryJobReport(JobDTO jobDTO, String processInstanceId) {
        if (jobDTO == null) {
            throw new BusinessException("未找到作业信息，jobId：" + jobDTO.getId() + "，procInsId：" + processInstanceId);
        }

        // 自动执行任务，直接从DB中获取
        if (isAutoExecJob(jobDTO.getSchedulerType())) {
            JobReportDTO jobReport = queryJobReportFromDb(jobDTO.getId(), jobDTO.getId());
            if (jobReport != null) {
                jobReport.setReportMap(JSONObject.parseObject(jobReport.getReportJson(), Map.class));
                return jobReport;
            }

            return null;
        }

        // step1:查询流程实例是否存在
        ProcessInstanceDto instance = getProcessInstance(jobDTO.getId(), processInstanceId == null ? jobDTO.getProcessInstanceId() : processInstanceId);
        if (instance == null) {
            logger.debug("未找到流程实例，jobId:" + jobDTO.getId());
            return null;
        }

        // step2:流程实例状态是否为success，只有sucess状态才有质量报告
        if (instance.getState() == null) {
            logger.error("作业状态为空，jobId：" + jobDTO.getId() + "，procInsId：" + processInstanceId);
            return null;
        }
        if (instance.getState() != JobState.SUCCESS.getCode()) {
            logger.debug("作业状态为{}，jobId：{}，procInsId：{}", JobState.getDescByCode(instance.getState()), jobDTO.getId(), processInstanceId);
            return null;
        }

        // step3:查表获取质量报告
        JobReportDTO jobReport = queryJobReportFromDb(jobDTO.getId(), instance.getId());
        if (jobReport != null) {
            jobReport.setReportMap(JSONObject.parseObject(jobReport.getReportJson(), Map.class));
            return jobReport;
        }

        // step4:表中没有质量报告，在线生成质量报告
        return generateJobReport(jobDTO, instance.getId());
    }

    /**
     * 下载质量报告
     *
     * @param jobDTO            质量作业信息
     * @param processInstanceId 根据流程实例ID下载历史任务执行结果
     * @param response
     * @return
     */
    public void downloadReport(JobDTO jobDTO, String processInstanceId, HttpServletResponse response) {
        if (jobDTO == null) {
            throw new BusinessException("作业不能为空");
        }

        // step1:获取质量报告参数列表
        JobReportDTO jobReport = queryJobReport(jobDTO, processInstanceId);
        if (jobReport == null) {
            logger.warn("质量作业执行失败，生成空的质量报告,jobId:{},insId:{}", jobDTO.getId(), processInstanceId);
            writeEmptyReport(jobDTO.getMeasureType(), response);
            return;
        }

        // step2:编排质量报告
        writeReport(jobDTO.getMeasureType(), jobReport, response);
    }

    /**
     * 重新生成质量报告
     * 注意：
     * 1、处理工单验证任务时，重新生成报告
     * 2、DDS向DGS推送任务执行结果时，生成报告
     *
     * @param jobDTO
     * @param processInstanceId
     */
    @Transactional
    public JobReportDTO regenerateReport(JobDTO jobDTO, String processInstanceId) {
        if (jobDTO == null) {
            logger.error("作业为null");
            return null;
        }
        if (StringUtils.isAnyEmpty(jobDTO.getId(), processInstanceId)) {
            logger.error("生成报告失败,jobId:{},insId:{}", jobDTO, processInstanceId);
            return null;
        }

        // 后生成
        JobReportDTO newReport = generateJobReport(jobDTO, processInstanceId);

        // 再持久化
        save2Db(newReport);

        // 推送系统消息
        sendSysMsg(jobDTO, newReport);

        return newReport;
    }

    /**
     * 统计质量告警次数（仅针对运行成功的 && 有告警的质量作业）
     *
     * @param startTime
     * @param endTime
     * @return
     */
    public Long countJobAlert(String startTime, String endTime, List<String> wordSecretList) {
        return this.mapper.countJobAlert(startTime, endTime, wordSecretList);
    }

    /**
     * 统计指定时间内的告警任务总数（去重）
     *
     * @param type
     * @param startTime
     * @param endTime
     * @return
     */
    public long countJobAlertByCond(Integer type, String startTime, String endTime) {
        return this.mapper.countJobAlertByCond(type, startTime, endTime);
    }

    /**
     * 查看质量评分趋势图
     *
     * @param timeMode,时间，0：近24h、1：近7天、2：近30天、3：本月
     * @param startTime
     * @param endTime
     * @return
     */
    public List<Map<String, Object>> queryScoreTrend(Integer timeMode, String startTime, String endTime, List<String> wordSecretList) {
        List<Map<String, Object>> resultList = new ArrayList<>();

        List<MetricDTO> queryList = null;
        switch (timeMode) {
            case 0:
                queryList = this.mapper.getLast24hScoreAvg(buildMetricQeuryCond(endTime), wordSecretList);
                break;
            case 1:
                queryList = this.mapper.getLast7DaysScoreAvg(wordSecretList);
                break;
            case 2:
                queryList = this.mapper.getLast30DaysScoreAvg(wordSecretList);
                break;
            case 3:
                queryList = this.mapper.getMonthScoreAvg(DateUtils.getDayByMonth(startTime), wordSecretList);
                break;
            default:
                throw new RuntimeException("不支持当前类型，timeMode：" + timeMode);
        }

        if (CollectionUtils.isNotEmpty(queryList)) {
            for (MetricDTO metric : queryList) {
                Map<String, Object> map = new HashMap<>();
                map.put("name", metric.getName());

                // 平均值保留两位有效数字
                if (metric.getValue() > 0) {
                    map.put("value", new BigDecimal(metric.getValue()).setScale(QaConstant.DEFAULT_SCALE, BigDecimal.ROUND_HALF_UP).doubleValue());
                } else {
                    map.put("value", metric.getValue());
                }

                resultList.add(map);
            }
        }

        return resultList;
    }

    /**
     * 查看质量告警趋势图
     *
     * @param timeMode,时间，0：近24h、1：近7天、2：近30天、3：本月
     * @param startTime
     * @param endTime
     * @return
     */
    public List<Map<String, Object>> queryAlertTrend(Integer timeMode, String startTime, String endTime, List<String> wordSecretList) {
        List<Map<String, Object>> resultList = new ArrayList<>();

        List<MetricDTO> queryList = null;
        switch (timeMode) {
            case 0:
                queryList = this.mapper.getLast24hAlertNum(buildMetricQeuryCond(endTime), wordSecretList);
                break;
            case 1:
                queryList = this.mapper.getLast7DaysAlertNum(wordSecretList);
                break;
            case 2:
                queryList = this.mapper.getLast30DaysAlertNum(wordSecretList);
                break;
            case 3:
                queryList = this.mapper.getMonthAlertNum(DateUtils.getDayByMonth(startTime), wordSecretList);
                break;
            default:
                throw new RuntimeException("不支持当前类型，timeMode：" + timeMode);
        }

        if (CollectionUtils.isNotEmpty(queryList)) {
            for (MetricDTO metric : queryList) {
                Map<String, Object> map = new HashMap<>();
                map.put("name", metric.getName());
                map.put("value", metric.getValue());
                resultList.add(map);
            }
        }

        return resultList;
    }

    /**
     * 查看质量评分分布图
     *
     * @param startTime
     * @param endTime
     * @return
     */
    public List<Map<String, Object>> queryScoreDistribution(String startTime, String endTime, List<String> wordSecretList) {
        List<Map<String, Object>> resultList = new ArrayList<>();
        for (ThreeTuple tuple : scoreGroup) {
            String name = (String) tuple.getFirst();
            Integer minScore = (Integer) tuple.getSecond();
            Integer maxScore = (Integer) tuple.getThird();

            long jobNum = this.mapper.countJobByScore(minScore, maxScore, startTime, endTime, wordSecretList);
            Map<String, Object> map = new HashMap<>();
            map.put("name", name);
            map.put("value", jobNum);
            resultList.add(map);
        }

        return resultList;
    }

    /**
     * 查看质量评分排名
     *
     * @param rankMode,排名方式，0：排名最高、1：排名最低"
     * @param limitNum                     排名总数，默认10
     * @param startTime
     * @param endTime
     * @return
     */
    public List<Map<String, Object>> queryScoreRank(Integer rankMode, Integer limitNum, String startTime, String endTime, List<String> wordSecretList) {
        List<Map<String, Object>> resultList = new ArrayList<>();

        List<JobReportDTO> reportList = this.mapper.getReportListByScore(rankMode, limitNum, startTime, endTime, wordSecretList);
        if (CollectionUtils.isNotEmpty(reportList)) {
            for (JobReportDTO report : reportList) {
                Map<String, Object> map = new HashMap<>();

                map.put("name", report.getJobName());

                // 平均值保留两位有效数字
                if (report.getScore() > 0) {
                    map.put("value", new BigDecimal(report.getScore()).setScale(QaConstant.DEFAULT_SCALE, BigDecimal.ROUND_HALF_UP).doubleValue());
                } else {
                    map.put("value", report.getScore());
                }

                resultList.add(map);
            }
        }

        return resultList;
    }

    /**
     * 对外接口：DDS向DGS推送任务执行结果时，触发生成质量报告
     */
    public void run() {
        logger.info("DGS开启后台质量报告生成任务...");

        executorService = Executors.newFixedThreadPool(QaConstant.JOB_REPORT_FIXED_THREAD_NUM);
        while (true) {
            try {
                // step1:当前队列为空，延时处理
                if (redisUtils.llen(QaConstant.JOB_REPORT_REDIS_KEY) <= 0) {
                    continue;
                }

                // step2:从redis中拉取一条生成报告CMD
                JobReportCommandDTO command = this.getJobReportCmdFromRedis();
                if (command == null) {
                    continue;
                }
                logger.info("从redis获取一条生成质量报告Command，：\n" + JSONObject.toJSONString(command));

                // step3:任务执行失败，不需要生成质量报告
                if (command.getState() != 7) {
                    logger.warn("任务执行失败，不需要生成质量报告，jobId:{},insId:{},state:{}", command.getJobId(), command.getProcessInstanceId(), command.getState());
                    continue;
                }

                // step4:生成质量报告
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        // dds推送任务执行结果时，processInstance自身状态可能还未更新完成，此处等待其更新完成
                        ProcessInstanceDto instance = getProcessInstance(command.getJobId(), command.getProcessInstanceId());
                        if (instance == null) {
                            logger.info("未找到流程实例，jobId:" + command.getJobId());
                            return;
                        }
                        if (instance.getState() != 7) {
                            logger.info("流程实例状态暂未更新，等待其更新成功，jobId:{},insId:{},state:{}", command.getJobId(), command.getProcessInstanceId(), instance.getState());
                            redisUtils.lpush(QaConstant.JOB_REPORT_REDIS_KEY, JsonHelper.getInstance().writeValueAsString(command));
                            return;
                        }

                        // 生成质量报告
                        regenerateReport(command2Job(command), command.getProcessInstanceId());
                    }
                });
            } catch (Exception e) {
                logger.error("后台从DB拉取生成报告命令失败,errMsg={}", e.getMessage());
            } finally {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * DDS向DGS推送任务执行结果时，触发生成质量报告命令，将Command存入DB中，后台定时拉取Command生成报告
     *
     * @param jobDTO
     */
    public void pushJobReportCommand2Redis(JobDTO jobDTO) {
        if (jobDTO == null) {
            logger.error("作业为null");
            return;
        }
        if (StringUtils.isAnyEmpty(jobDTO.getId(), jobDTO.getProcessInstanceId())) {
            logger.error("保存生成报告命令失败，作业ID：{}, 流程实例ID：{}", jobDTO.getId(), jobDTO.getProcessInstanceId());
            return;
        }

        // 向redis消息队列插入一条记录，后台自动生成工单
        JobReportCommandDTO commandDTO = job2Command(jobDTO);
        redisUtils.lpush(QaConstant.JOB_REPORT_REDIS_KEY, JsonHelper.getInstance().writeValueAsString(commandDTO));

        logger.info("任务执行完毕，提交生成报告Command成功\n.{}", JSONObject.toJSONString(commandDTO));
    }

    /**
     * 设置文件校验任务质量报告（自动执行）
     *
     * @param jobDTO
     * @param ruleList
     * @param result
     * @param execTime
     */
    public void buildAutoExecFileJobReport(JobDTO jobDTO, List<RuleDTO> ruleList,
                                           ResponseMsg<List<FtdpFileDTO>> result, String execTime) {
        // step1:填充任务信息
        JobReportDTO jobReportDTO = fillAutoExecFileJobInfo(jobDTO, execTime);

        // step2:填充评分信息
        fillAutoExecScoreInfo(jobReportDTO, ruleList.size());

        // step3:填充文件信息
        fillAutoExecFileInfo(jobReportDTO, result.getResponseBody());

        // step4:填充规则详情
        fillAutoExecRuleInfo(jobDTO, ruleList, result.getResponseBody(), jobReportDTO);

        // step4:报告持久化
        save2Db(jobReportDTO);
    }

    /**
     * 生成质量报告
     *
     * @param jobDTO
     * @param processInstanceId
     * @return
     */
    private JobReportDTO generateJobReport(JobDTO jobDTO, String processInstanceId) {
        JobReportDTO jobReportDTO = null;

        try {
            // 依据校验类型生成报告，0:单表校验、1:跨表比对、2:跨库比对、3:文件校验、4:单指标校验、5:多指标校验、6:根据数据标准校验
            MeasureType measureType = MeasureType.getTypeByCode(jobDTO.getMeasureType());
            if (measureType == null) {
                throw new BusinessException("不支持该校验类型：" + jobDTO.getMeasureType() + ",获取质量报告失败,jobId:" + jobDTO.getId());
            }
            switch (measureType) {
                case SINGLE_SOURCE:
                case MULTI_SOURCE:
                    jobReportDTO = generateStructuredDataReport(jobDTO.getId(), processInstanceId, jobDTO);
                    break;
                case MULTI_DATABASE:
                case FILE:
                    jobReportDTO = generateFileReport(jobDTO.getId(), processInstanceId, jobDTO);
                    break;
                case SINGLE_METRIC:
                case MULTI_METRIC:
                case DATA_STANDARD:
                    throw new BusinessException("暂不支持" + measureType.getDesc() + "质量报告.");
                default:
                    throw new BusinessException("不支持该校验类型：" + measureType);
            }
        } catch (Exception e) {
            logger.error("生成质量报告失败，errMsg:", e);
        }

        return jobReportDTO;
    }

    /**
     * 生成结构化数据质量报告
     * 注意：
     * 1、支持单表校验
     * 2、支持跨表校验
     *
     * @param jobId
     * @param processInstanceId
     * @param jobDTO
     * @return
     */
    private JobReportDTO generateStructuredDataReport(String jobId, String processInstanceId, JobDTO jobDTO) {
        // step1:获取流程实例信息
        ProcessInstanceDto instance = getProcessInstance(jobId, processInstanceId == null ? jobDTO.getProcessInstanceId() : processInstanceId);

        // step2:获取执行结果列表
        List<JobResultDTO> jobResultList = getRuleResultList(instance);

        // step3:填充任务信息
        JobReportDTO jobReportDTO = fillReportJobInfo(instance, jobResultList);

        // step4:填充表信息
        fillReportTableInfo(jobResultList, jobReportDTO);

        // step5:填充质量评分和规则详情
        fillReportRuleDetails(instance, jobResultList, jobReportDTO, MeasureType.getTypeByCode(jobDTO.getMeasureType()));

        return jobReportDTO;
    }

    /**
     * 文件校验质量报告
     *
     * @param jobId
     * @param processInstanceId
     * @param jobDTO
     * @return
     */
    private JobReportDTO generateFileReport(String jobId, String processInstanceId, JobDTO jobDTO) {
        // step1:获取流程实例信息
        ProcessInstanceDto instance = getProcessInstance(jobId, processInstanceId == null ? jobDTO.getProcessInstanceId() : processInstanceId);

        // step2:获取执行结果列表
        List<JobResultDTO> jobResultList = getRuleResultList(instance);

        // step3:填充任务信息
        JobReportDTO jobReportDTO = fillReportJobInfo(instance, jobResultList);

        // step4:填充文件信息
        fillReportFileInfo(jobResultList, jobReportDTO);

        // step5:填充质量评分和规则详情
        fillReportRuleDetails(instance, jobResultList, jobReportDTO, MeasureType.getTypeByCode(jobDTO.getMeasureType()));

        return jobReportDTO;
    }

    /**
     * 获取流程实例
     *
     * @param jobId
     * @param processInstanceId
     * @return
     */
    private ProcessInstanceDto getProcessInstance(String jobId, String processInstanceId) {
        if (StringUtils.isBlank(jobId)) {
            logger.warn("质量作业ID为空,查询流程实例失败.");
            return null;
        }

        // 流程实例ID为空，查询最近一次执行结果
        if (StringUtils.isEmpty(processInstanceId)) {
            return this.commonDao.getLatestProcessInstanceByProcessDefinitionId(jobId);
        }

        // 流程实例ID不为空，需要匹配
        Page<ProcessInstanceDto> queryRet = this.commonDao.queryProcessInstanceListPaging(jobId, null, null, null, null, null, null);
        if (CollectionUtils.isEmpty(queryRet)) {
            logger.debug("未找到流程实例,jobId=" + jobId + ",instanceId=" + processInstanceId);
            return null;
        }

        for (ProcessInstanceDto ins : queryRet.getResult()) {
            if (ins == null || StringUtils.isEmpty(ins.getId())) {
                continue;
            }

            if (ins.getId().equals(processInstanceId)) {
                return ins;
            }
        }

        logger.debug("未找到流程实例,jobId=" + jobId + ",instanceId=" + processInstanceId);

        return null;
    }

    /**
     * 填充质量报告任务信息
     *
     * @param instance
     * @param jobResultList
     * @return
     */
    private JobReportDTO fillReportJobInfo(ProcessInstanceDto instance, List<JobResultDTO> jobResultList) {
        if (instance == null) {
            logger.debug("流程定义为null.");
            return null;
        }
        if (CollectionUtils.isEmpty(jobResultList)) {
            logger.debug("校验结果列表为空.");
            return null;
        }
        if (jobResultList.get(0) == null && StringUtils.isEmpty(jobResultList.get(0).getJobInfo())) {
            logger.debug("校验结果中任务信息为空.");
            return null;
        }

        // 任务名称、校验类型、调度类型
        Map<String, Object> jobInfoMap = Maps.newHashMap();
        JobDTO jobInfo = JSONObject.parseObject(jobResultList.get(0).getJobInfo(), JobDTO.class);
        if (jobInfo != null) {
            MeasureType measureType = MeasureType.getTypeByCode(jobInfo.getMeasureType());
            SchedulerType schedulerType = SchedulerType.getTypeByCode(jobInfo.getSchedulerType());
            jobInfoMap.put(QaConstant.JOB_NAME, jobInfo.getName() == null ? QaConstant.DOUBLE_HYPHEN : jobInfo.getName());
            jobInfoMap.put(QaConstant.MEASURE_TYPE_NAME, measureType == null ? QaConstant.DOUBLE_HYPHEN : measureType.getDesc());
            jobInfoMap.put(QaConstant.SCHEDULER_TYPE_NAME, schedulerType == null ? QaConstant.DOUBLE_HYPHEN : schedulerType.getDesc());
        }
        // 任务执行时间
        String executeTime = QaConstant.DOUBLE_HYPHEN;
        if (instance != null && instance.getStartTime() != null && instance.getEndTime() != null) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Constants.YYYY_MM_DD_HH_MM_SS);
            executeTime = simpleDateFormat.format(instance.getStartTime()) + " 至 " + simpleDateFormat.format(instance.getEndTime());
        }

        // 文件校验试飞信息，包括类别、型号、机号、飞行日期、架次、试飞类型
        if (jobInfo.getMeasureType().equals(MeasureType.FILE.getCode())) {
            jobInfoMap.put(QaConstant.TYPE_NAME, jobInfo.getTypeName() == null ? QaConstant.DOUBLE_HYPHEN : jobInfo.getTypeName());
            jobInfoMap.put(QaConstant.MODEL_CODE, jobInfo.getModelCode() == null ? QaConstant.DOUBLE_HYPHEN : jobInfo.getModelCode());
            jobInfoMap.put(QaConstant.AIRCRAFT_NAME, jobInfo.getAircraftName() == null ? QaConstant.DOUBLE_HYPHEN : jobInfo.getAircraftName());
            jobInfoMap.put(QaConstant.FLIGHT_DATE, jobInfo.getFlightDate() == null ? QaConstant.DOUBLE_HYPHEN : jobInfo.getFlightDate());
            jobInfoMap.put(QaConstant.SORTIE, jobInfo.getSortie() == null ? QaConstant.DOUBLE_HYPHEN : jobInfo.getSortie());
            jobInfoMap.put(QaConstant.TEST_SYSTEM_NAME, jobInfo.getTestSystemName() == null ? QaConstant.DOUBLE_HYPHEN : jobInfo.getTestSystemName());
        }

        jobInfoMap.put(QaConstant.EXECUTE_TIME, executeTime);
        Map<String, Object> paraMap = new HashMap<>();
        paraMap.put(QaConstant.REPORT_JOB_INFO, jobInfoMap);

        JobReportDTO jobReportDTO = new JobReportDTO();
        jobReportDTO.setJobId(instance.getProcessDefinitionId());
        jobReportDTO.setJobName(instance.getName());
        jobReportDTO.setProcessInstanceId(instance.getId());
        jobReportDTO.setReportMap(paraMap);

        return jobReportDTO;
    }

    /**
     * 填充质量报告表信息
     *
     * @param jobResultList
     * @param jobReportDTO
     */
    private void fillReportTableInfo(List<JobResultDTO> jobResultList, JobReportDTO jobReportDTO) {
        if (jobReportDTO == null) {
            logger.debug("生成任务信息失败.");
            return;
        }
        if (CollectionUtils.isEmpty(jobResultList)) {
            logger.debug("校验结果列表为空.");
            return;
        }
        if (jobResultList.get(0) == null && StringUtils.isEmpty(jobResultList.get(0).getTableInfo())) {
            logger.debug("校验结果中表信息为空.");
            return;
        }

        List<List<Object>> talbeInfoList = new ArrayList<>();
        List<Map<String, Object>> tableInfoMap = JSONObject.parseObject(jobResultList.get(0).getTableInfo(), List.class);
        if (CollectionUtils.isNotEmpty(tableInfoMap)) {
            for (Map<String, Object> map : tableInfoMap) {
                if (map == null) {
                    continue;
                }

                List<Object> valueList = new ArrayList<>();
                valueList.add(map.get(QaConstant.SEQ_NUM));
                valueList.add(map.get(QaConstant.TABLE_NAME));
                valueList.add(map.get(QaConstant.DATASOURCE_NAME));
                valueList.add(map.get(QaConstant.DB_NAME));
                valueList.add(map.get(QaConstant.DATASOURCE_TYPE));
                valueList.add(map.get(QaConstant.ROW_NUM));

                talbeInfoList.add(valueList);
            }
        }

        Map<String, Object> paraMap = jobReportDTO.getReportMap();
        paraMap.put(QaConstant.REPORT_TABLE_INFO, talbeInfoList);
    }

    /**
     * 填充质量报告文件信息
     *
     * @param jobResultList
     * @param jobReportDTO
     */
    private void fillReportFileInfo(List<JobResultDTO> jobResultList, JobReportDTO jobReportDTO) {
        if (jobReportDTO == null) {
            logger.debug("生成任务信息失败.");
            return;
        }
        if (CollectionUtils.isEmpty(jobResultList)) {
            logger.debug("校验结果列表为空.");
            return;
        }
        if (jobResultList.get(0) == null && StringUtils.isEmpty(jobResultList.get(0).getTableInfo())) {
            logger.debug("校验结果中表信息为空.");
            return;
        }

        List<List<Object>> fileInfoList = new ArrayList<>();
        List<Map<String, Object>> fileInfoMap = JSONObject.parseObject(jobResultList.get(0).getTableInfo(), List.class);
        if (CollectionUtils.isNotEmpty(fileInfoMap)) {
            for (Map<String, Object> map : fileInfoMap) {
                if (map == null) {
                    continue;
                }

                List<Object> valueList = new ArrayList<>();
                valueList.add(map.get(QaConstant.SEQ_NUM));
                valueList.add(map.get(QaConstant.NAME));
                valueList.add(map.get(QaConstant.SIZE));
                valueList.add(map.get(QaConstant.CREATION_DATE));

                fileInfoList.add(valueList);
            }
        }

        Map<String, Object> paraMap = jobReportDTO.getReportMap();
        paraMap.put(QaConstant.REPORT_FILE_INFO, fileInfoList);
    }

    /**
     * 填充质量报告规则详情
     *
     * @param instance
     * @param jobResultList
     * @param jobReportDTO
     * @param measureType
     */
    private void fillReportRuleDetails(ProcessInstanceDto instance, List<JobResultDTO> jobResultList,
                                       JobReportDTO jobReportDTO, MeasureType measureType) {
        if (jobReportDTO == null) {
            logger.debug("生成任务信息失败.");
            return;
        }

        Map<String, Object> paraMap = jobReportDTO.getReportMap();

        // 计算每条规则执行情况，包括合格条数、不合格条数、合格率、得分，最后计算出总分、告警数、校验结果
        int ruleCnt = 1;// 规则详情中序号
        int cnt = 1; // 保证总分小于等于100
        Map<String, Object> statMap = new HashMap<>();
        List<Object> ruleDetailList = Lists.newArrayList();
        for (JobResultDTO jobResult : jobResultList) {
            if (jobResult == null) {
                continue;
            }

            ruleDetailList.add(buildSingleRuleCalcResult(jobResult, ruleCnt++, statMap, measureType, cnt == jobResultList.size()));
            cnt++;
        }

        Map<String, Object> scoreInfoMap = Maps.newHashMap();
        Double totalScore = statMap.get(QaConstant.SCORE) == null ? 0.0 : (Double) statMap.get(QaConstant.SCORE);
        scoreInfoMap.put(QaConstant.SCORE, new BigDecimal(totalScore).setScale(QaConstant.DEFAULT_SCALE, BigDecimal.ROUND_HALF_UP).doubleValue());
        scoreInfoMap.put(QaConstant.RULE_NUM, ruleCnt - 1);
        Integer alertNum = (Integer) statMap.get(QaConstant.ALERT_NUM);
        scoreInfoMap.put(QaConstant.ALERT_NUM, alertNum == null ? 0 : alertNum);

        if ((instance == null) || (instance != null && instance.getState() != 7) || (ruleCnt - 1) <= 0) {
            // 任务未执行 || 任务执行了但是未成功 || 没有质量规则，统一设置状态为“--”
            scoreInfoMap.put(QaConstant.RESULT, QaConstant.DOUBLE_HYPHEN);
            jobReportDTO.setJobResultType(JobResultType.FAIL.getCode());
        } else {
            // 简化处理，任意一个rule告警就认为本次任务“告警”
            if (alertNum != null && alertNum > 0) {
                scoreInfoMap.put(QaConstant.RESULT, QaConstant.JOB_ALERT);
                jobReportDTO.setJobResultType(JobResultType.ALERT.getCode());
            } else {
                scoreInfoMap.put(QaConstant.RESULT, QaConstant.JOB_NORAML);
                jobReportDTO.setJobResultType(JobResultType.NORMAL.getCode());
            }
        }

        jobReportDTO.setScore(totalScore);
        paraMap.put(QaConstant.SCORE_INFO, scoreInfoMap);
        paraMap.put(QaConstant.RULE_DETAIL_INFO, ruleDetailList);
        jobReportDTO.setReportJson(JSONObject.toJSONString(paraMap));
    }

    /**
     * 构建跨表校验-单条规则校验结果
     * 注意：
     * 1、依次包含以下内容：序号、规则名称、模板名称、模板来源、权重、校验表/校验字段、校验规则、实际结果、得分、状态（正常、告警）
     *
     * @param jobResult
     * @param seqNum
     * @param statMap
     * @param measureType
     * @param lastRuleFlag
     * @return
     */
    @SuppressWarnings("屎山代码，方法冗长、复杂度高，待优化。2023-09-25 WM")
    private List<Object> buildSingleRuleCalcResult(JobResultDTO jobResult, int seqNum, Map<String, Object> statMap,
                                                   MeasureType measureType, boolean lastRuleFlag) {
        List<Object> resultList = Lists.newArrayList();

        // 设置序号、规则名称、模板名称、模板来源、模板类型、权重、校验表/校验字段、校验规则、实际值
        Object actualValue = jobResult.getActualValue();
        resultList.add(seqNum);
        resultList.add(jobResult.getRuleName());
        resultList.add(jobResult.getTemplateName() == null ? QaConstant.DOUBLE_HYPHEN : jobResult.getTemplateName());
        resultList.add(jobResult.getTemplateType() == null ? QaConstant.DOUBLE_HYPHEN : TemplateType.getDescByCode(jobResult.getTemplateType()));
        resultList.add(jobResult.getWeight() == null ? 0 : jobResult.getWeight());
        if (measureType == MeasureType.SINGLE_SOURCE) {
            resultList.add(jobResult.getCheckField() == null ? QaConstant.DOUBLE_HYPHEN : jobResult.getCheckField());
            resultList.add(StringUtils.isEmpty(jobResult.getAlarmFormula()) ? QaConstant.DOUBLE_HYPHEN : jobResult.getAlarmFormula());
            resultList.add(actualValue == null ? QaConstant.DOUBLE_HYPHEN : actualValue);
        } else if (measureType == MeasureType.MULTI_SOURCE) {
            resultList.add(jobResult.getCheckTable() == null ? QaConstant.DOUBLE_HYPHEN : jobResult.getCheckTable());
            resultList.add(StringUtils.isEmpty(jobResult.getAlarmFormula()) ? QaConstant.DOUBLE_HYPHEN : jobResult.getAlarmFormula());
            resultList.add(actualValue == null ? QaConstant.DOUBLE_HYPHEN : actualValue);
        } else if (measureType == MeasureType.FILE) {
            resultList.add(jobResult.getCheckTable() == null ? QaConstant.DOUBLE_HYPHEN : jobResult.getCheckTable()); // 校验成功文件，保存在checkTable中，不想变动表结构
            resultList.add(jobResult.getCheckField() == null ? QaConstant.DOUBLE_HYPHEN : jobResult.getCheckField()); // 校验失败文件，保存在checkField中，不想变动表结构
        } else {
            resultList.add(QaConstant.DOUBLE_HYPHEN);
        }

        double score = 0.0; // 得分
        Double weightRatio = jobResult.getWeightRatio(); // 权重占比
        String ruleState = null; // 该条规则告警结果
        if (measureType == MeasureType.FILE) {
            // 存在校验失败文件，该条规则告警
            if (StringUtils.isNotEmpty(jobResult.getCheckField())) {
                ruleState = QaConstant.JOB_ALERT;
                Integer alertNum = (Integer) statMap.get(QaConstant.ALERT_NUM);
                if (alertNum == null) {
                    statMap.put(QaConstant.ALERT_NUM, 1);
                } else {
                    statMap.put(QaConstant.ALERT_NUM, alertNum + 1);
                }
            } else {
                ruleState = QaConstant.JOB_NORAML;
            }
        } else {
            // 结构化数据校验
            if (actualValue != null) {
                if (isNumeric(actualValue)) {
                    String formula = jobResult.getAlarmFormula();
                    if (!checkFormulaValidity(formula)) {
                        ruleState = QaConstant.JOB_NORAML;
                    } else {
                        Map<String, Object> env = new HashMap<String, Object>();
                        env.put(QaConstant.TASK_RESULT_FIELD_NAME, Double.parseDouble(actualValue.toString()));
                        if ((Boolean) AviatorEvaluator.execute(formula, env)) {
                            ruleState = QaConstant.JOB_NORAML;
                        } else {
                            Integer alertNum = (Integer) statMap.get(QaConstant.ALERT_NUM);
                            if (alertNum == null) {
                                statMap.put(QaConstant.ALERT_NUM, 1);
                            } else {
                                statMap.put(QaConstant.ALERT_NUM, alertNum + 1);
                            }
                            ruleState = QaConstant.JOB_ALERT;
                        }
                    }
                } else {
                    ruleState = QaConstant.JOB_NORAML;
                }
            } else {
                ruleState = QaConstant.JOB_NORAML;
            }
        }

        if (ruleState == QaConstant.JOB_NORAML) {
            score = new BigDecimal(100 * weightRatio).setScale(QaConstant.DEFAULT_SCALE, BigDecimal.ROUND_HALF_UP).doubleValue();
        }

        // 最后一个规则特殊处理，保证总分不超过100
        Double totalScore = (Double) statMap.get(QaConstant.SCORE);
        if (totalScore == null) {
            statMap.put(QaConstant.SCORE, score);
        } else {
            if (lastRuleFlag) {
                if (totalScore + score > 100) {
                    score = 100 - totalScore;
                    if (score < 0) {
                        score = 0.0;
                    }
                }
            }

            statMap.put(QaConstant.SCORE, totalScore + score);
        }

        // 设置得分、状态
        resultList.add(score); // 得分
        resultList.add(ruleState); // 状态

        return resultList;
    }

    /**
     * 获取质量规则执行结果Map
     * 注意：会对同一个流程实例的执行结果去重，以最近执行结果为主
     *
     * @param instance
     * @return
     */
    private List<JobResultDTO> getRuleResultList(ProcessInstanceDto instance) {
        if (instance == null) {
            logger.info("未找到流程实例");
            return new ArrayList<>();
        }

        List<JobResultDTO> jobResultList = Lists.newArrayList();
        Map<String, JobResultDTO> ruleResultMap = new HashMap<>();
        if (instance != null && instance.getState() == 7) {
            jobResultList = this.jobResultService.getResultList(instance.getProcessDefinitionId(), instance.getId());
            if (CollectionUtils.isEmpty(jobResultList)) {
                logger.debug("未找到质量任务执行结果,jobId=" + instance.getProcessDefinitionId() + ",instanceId=" + instance.getId());
                return new ArrayList<>();
            }

            for (JobResultDTO jobResult : jobResultList) {
                String ruleId = jobResult.getRuleId();
                if (StringUtils.isBlank(ruleId)) {
                    continue;
                }
                if (!ruleResultMap.containsKey(ruleId)) {
                    ruleResultMap.put(ruleId, jobResult);
                    continue;
                }

                // 工单验证任务会多次执行同一个processInstance，结果表中会出现多个job_id、process_instance_id、rule_id相同的记录，所以要以最近一次执行结果为主；
                JobResultDTO oldJobResult = ruleResultMap.get(ruleId);
                if (oldJobResult == null) {
                    continue;
                }
                if (oldJobResult.getCreationDate() == null || jobResult.getCreationDate() == null) {
                    continue;
                }
                if (oldJobResult.getCreationDate().compareTo(jobResult.getCreationDate()) < 0) {
                    ruleResultMap.put(ruleId, jobResult);
                }
            }
        }

        if (ruleResultMap != null && ruleResultMap.size() > 0) {
            return ruleResultMap.values().stream().collect(Collectors.toList());
        }

        return new ArrayList<>();
    }

    /**
     * 检查质量规则告警表达式合法性,true:合法、false:非法
     *
     * @param formula
     * @return
     */
    private boolean checkFormulaValidity(String formula) {
        if (StringUtils.isEmpty(formula)) {
            return false;
        }

        // 非法表达式统一认定为false
        boolean flag = true;
        try {
            AviatorEvaluator.compile(formula);
        } catch (Exception e) {
            logger.debug("质量告警表达式非法，{}", formula);
            flag = false;
        }

        return flag;
    }

    /**
     * 判断指定字符串是否为数值类型
     * 注意：
     * 1、规则校验结果可能为空、字符串、数值，只有是数值类型才能计算
     *
     * @param value
     * @return
     */
    private boolean isNumeric(Object value) {
        if (value == null) {
            return false;
        }

        if (StringUtils.isEmpty(value.toString())) {
            return false;
        }

        boolean flag = true;
        try {
            Double.parseDouble(value.toString());
        } catch (NumberFormatException e) {
            flag = false;
        }

        return flag;
    }

    /**
     * 编排质量报告
     *
     * @param type
     * @param jobReport
     * @param response
     */
    private void writeReport(Integer type, JobReportDTO jobReport, HttpServletResponse response) {
        MeasureType measureType = MeasureType.getTypeByCode(type);
        switch (measureType) {
            case SINGLE_SOURCE:
            case MULTI_SOURCE:
                writeStructDataReport(jobReport, response);
                break;
            case FILE:
                writeFileReport(jobReport, response);
                break;
            default:
                throw new RuntimeException("不支持当前当前校验类型：" + measureType);
        }
    }

    /**
     * 生成单表校验/多表校验质量报告文件
     *
     * @param jobReport
     * @param response
     */
    private void writeStructDataReport(JobReportDTO jobReport, HttpServletResponse response) {
        Map<String, Object> paraMap = jobReport.getReportMap();

        // step1:获取判读报告模板
        XWPFDocument xwpfDocument = getTemplateFile(REPORT_TMEPLATE_NAME);

        // step2:填充报告基本信息
        fillStructDataBaseInfo(xwpfDocument, paraMap);

        // step3:填充表
        fillStructDataDataTables(xwpfDocument, paraMap);

        // step4:填充图表
        drawStructDataEcharts(xwpfDocument, paraMap);

        // step5:生成判读报告
        generateNewFile(xwpfDocument, paraMap, response);
    }

    /**
     * 生成单表校验/多表校验质量报告文件
     *
     * @param jobReport
     * @param response
     */
    private void writeFileReport(JobReportDTO jobReport, HttpServletResponse response) {
        Map<String, Object> paraMap = jobReport.getReportMap();

        // step1:获取判读报告模板
        XWPFDocument xwpfDocument = getTemplateFile(FILE_REPORT_TMEPLATE_NAME);

        // step2:填充报告基本信息
        fillStructDataBaseInfo(xwpfDocument, paraMap);

        // step3:填充表
        fillFiles(xwpfDocument, paraMap);

        // step4:填充图表
        drawStructDataEcharts(xwpfDocument, paraMap);

        // step5:生成判读报告
        generateNewFile(xwpfDocument, paraMap, response);
    }

    /**
     * 编排质量空报告
     *
     * @param type
     * @param response 判读报告流
     */
    private void writeEmptyReport(Integer type, HttpServletResponse response) {
        // step1:获取空报告模板名称
        String emptyReportName = null;
        MeasureType measureType = MeasureType.getTypeByCode(type);
        switch (measureType) {
            case SINGLE_SOURCE:
            case MULTI_SOURCE:
                emptyReportName = EMPTY_REPORT_TMEPLATE_NAME;
                break;
            case FILE:
                emptyReportName = FILE_EMPTY_REPORT_TMEPLATE_NAME;
                break;
            default:
                throw new RuntimeException("不支持当前当前校验类型：" + measureType);
        }

        // step2:获取空报告模板
        XWPFDocument xwpfDocument = getTemplateFile(emptyReportName);

        // step3:生成质量报告
        generateNewFile(xwpfDocument, new HashMap<>(), response);
    }

    /**
     * 获取判读报告模板
     *
     * @return
     */
    private XWPFDocument getTemplateFile(String fileName) {
        InputStream ins = this.getClass().getClassLoader().getResourceAsStream(fileName);
        if (ins == null) {
            logger.error(String.format("未获取到模板文件[%s]", fileName));
            return null;
        }

        XWPFDocument xwpfDocument = null;
        try {
            xwpfDocument = new XWPFDocument(ins);
        } catch (IOException e) {
            logger.error("读取模板文件[" + fileName + "]失败,errMsg=" + e.getMessage());
            throw new BusinessException(String.format("读取模板文件[%s]", fileName));
        }

        return xwpfDocument;
    }

    /**
     * 填充单表校验/多表校验模板中基本信息，包括作业信息、得分信息
     *
     * @param xwpfDocument
     * @param paraMap
     */
    private void fillStructDataBaseInfo(XWPFDocument xwpfDocument, Map<String, Object> paraMap) {
        if (xwpfDocument == null || paraMap == null || paraMap.size() < 1) {
            return;
        }

        Map<String, Object> varMap = new HashMap<>();
        Map<String, Object> jobInfoMap = (Map<String, Object>) paraMap.get("jobInfo");
        Map<String, Object> scoreInfoMap = (Map<String, Object>) paraMap.get("scoreInfo");
        varMap.putAll(jobInfoMap);
        varMap.putAll(scoreInfoMap);

        replaceAllVariables(xwpfDocument, varMap);
    }

    /**
     * 填充文件模板中图表信息，例如评分趋势图、告警趋势图等
     *
     * @param xwpfDocument
     * @param paraMap
     */
    private void drawStructDataEcharts(XWPFDocument xwpfDocument, Map<String, Object> paraMap) {
        //获取word中所有图表对象
        List<XWPFChart> charts = xwpfDocument.getCharts();
        if (CollectionUtils.isEmpty(charts)) {
            return;
        }

        // 填充得分趋势
        List<String> wordSecretList = ThreadContextHelper.getWordSecretList();
        List<Map<String, Object>> scoreTrendList = this.queryScoreTrend(0, null, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), wordSecretList);
        if (CollectionUtils.isNotEmpty(scoreTrendList)) {
            String[] singleBarSeriesNames = {"评分趋势"};
            setEchart(charts.get(0), scoreTrendList, "质量评分趋势图", singleBarSeriesNames);
        }

        // 填充告警趋势
        List<Map<String, Object>> alertTrendList = this.queryAlertTrend(0, null, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), wordSecretList);
        if (CollectionUtils.isNotEmpty(alertTrendList)) {
            String[] singleBarSeriesNames = {"告警趋势"};
            setEchart(charts.get(1), alertTrendList, "质量告警趋势图", singleBarSeriesNames);
        }
    }

    /**
     * 填充判读结果
     *
     * @param xwpfDocument
     * @param paraMap
     */
    private void fillStructDataDataTables(XWPFDocument xwpfDocument, Map<String, Object> paraMap) {
        if (xwpfDocument == null || paraMap == null || paraMap.size() < 1) {
            return;
        }

        // 获取表格数据
        List<XWPFTable> tables = xwpfDocument.getTables();
        if (CollectionUtils.isEmpty(tables)) {
            return;
        }

        // 填充表信息
        List<List<Object>> tableList = (List<List<Object>>) paraMap.get("tableInfo");
        if (CollectionUtils.isNotEmpty(tableList)) {
            fillWordTableInfo(tables.get(0), tableList);
        }

        // 填充规则信息
        List<List<Object>> ruleList = (List<List<Object>>) paraMap.get("ruleDetailInfo");
        if (CollectionUtils.isNotEmpty(ruleList)) {
            fillWordTableInfo(tables.get(1), ruleList);
        }
    }

    /**
     * 填充判读结果
     *
     * @param xwpfDocument
     * @param paraMap
     */
    private void fillFiles(XWPFDocument xwpfDocument, Map<String, Object> paraMap) {
        if (xwpfDocument == null || paraMap == null || paraMap.size() < 1) {
            return;
        }

        // 获取表格数据
        List<XWPFTable> tables = xwpfDocument.getTables();
        if (CollectionUtils.isEmpty(tables)) {
            return;
        }

        // 填充表信息
        List<List<Object>> tableList = (List<List<Object>>) paraMap.get("fileInfo");
        if (CollectionUtils.isNotEmpty(tableList)) {
            fillWordTableInfo(tables.get(0), tableList);
        }

        // 填充规则信息
        List<List<Object>> ruleList = (List<List<Object>>) paraMap.get("ruleDetailInfo");
        if (CollectionUtils.isNotEmpty(ruleList)) {
            fillWordTableInfo(tables.get(1), ruleList);
        }
    }

    /**
     * 填充判读报告基本信息
     * 注意：基本信息包括任务名称、判读人、判读时间、机型、架次等
     *
     * @param xwpfDocument
     * @param paraMap
     */
    private void replaceAllVariables(XWPFDocument xwpfDocument, Map<String, Object> paraMap) {
        if (xwpfDocument == null || paraMap == null || paraMap.size() < 1) {
            return;
        }

        Iterator<XWPFParagraph> itPara = xwpfDocument.getParagraphsIterator();
        while (itPara.hasNext()) {
            XWPFParagraph paragraph = itPara.next();
            List<XWPFRun> runs = paragraph.getRuns();
            for (XWPFRun currRun : runs) {
                // 跳过空行
                if (currRun == null) {
                    continue;
                }

                // 替换掉关键字
                String sectionItem = currRun.getText(currRun.getTextPosition());
                if (StringUtils.isNotEmpty(sectionItem)) {
                    for (String keyWord : paraMap.keySet()) {
                        sectionItem = sectionItem.replace(keyWord, String.valueOf(paraMap.get(keyWord)));
                    }
                }
                currRun.setText(sectionItem, 0);
            }
        }
    }

    /**
     * 填充Word文档中表格信息
     *
     * @param table
     * @param valueList
     */
    private void fillWordTableInfo(XWPFTable table, List<List<Object>> valueList) {
        // 跳过表头，新增行
        for (int i = 1; i <= valueList.size(); i++) {
            XWPFTableRow row = table.createRow();
        }

        List<XWPFTableRow> rowList = table.getRows();
        for (int i = 1; i <= valueList.size(); i++) {
            XWPFTableRow xwpfTableRow = rowList.get(i);
            List<XWPFTableCell> tableCells = xwpfTableRow.getTableCells();
            List<Object> values = valueList.get(i - 1);
            if (values.size() != tableCells.size()) {
                logger.warn("质量计算结果列表和word文档中列表不匹配,valueList=" + values.size() + "cellSize=" + tableCells.size());
                continue;
            }

            for (int j = 0; j < tableCells.size(); j++) {
                XWPFTableCell xwpfTableCell = tableCells.get(j);
                xwpfTableCell.setText(values.get(j).toString());
            }
        }
    }

    /**
     * 生成判读报告文件
     *
     * @param xwpfDocument
     * @param paraMap
     * @param response
     */
    private void generateNewFile(XWPFDocument xwpfDocument, Map<String, Object> paraMap, HttpServletResponse response) {
        OutputStream outputStream = null;

        String fileName = "数据质量报告.docx";
        if (paraMap != null && paraMap.size() > 0) {
            Map<String, Object> basicInfoMap = (Map<String, Object>) paraMap.get("basicInfo");
            if (basicInfoMap != null) {
                String taskName = basicInfoMap.get("jobName") == null ? "数据质量" : (String) (basicInfoMap.get("jobName"));
                fileName = taskName + "-质量报告.docx";
            }
        }

        try {
            // step1:设置消息头
            response.setContentType("application/octet-stream");
            String downLoadfileName = URLEncoder.encode(fileName, CharEncoding.UTF_8);
            downLoadfileName = downLoadfileName.replace("+", "%20");// 解决URLEncoder.encode空格变+号问题
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + downLoadfileName);

            logger.info("生成的判读报告：[{}]...", downLoadfileName);

            // step2：返回数据流
            outputStream = response.getOutputStream();
            xwpfDocument.write(outputStream);
        } catch (Exception e) {
            throw new BusinessException(("生成数据质量评估报告失败,fileName=" + fileName + ",errMsg=" + e.getMessage()));
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (Exception e) {
                throw new BusinessException(("生成数据质量评估报告失败,errMsg=" + e.getMessage()));
            }
        }
    }

    /**
     * 填充Echart
     *
     * @param chart
     * @param dataList
     */
    private void setEchart(XWPFChart chart, List<Map<String, Object>> dataList, String title, String[] singleBarSeriesNames) {
        try {
            // 填充数据
            XSSFWorkbook workbook = chart.getWorkbook();
            XSSFSheet sheet = workbook.getSheetAt(0);
            if (CollectionUtils.isNotEmpty(dataList)) {
                doEchart(sheet, dataList);
            }

            //系列信息
            wordExportChar(chart, title, singleBarSeriesNames, sheet, 25);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 填充文件模板中图表数据
     *
     * @param sheet
     * @param dataList
     */
    private void doEchart(XSSFSheet sheet, List<Map<String, Object>> dataList) {
        if (CollectionUtils.isNotEmpty(dataList)) {
            int inx = 1;
            for (Map<String, Object> data : dataList) {
                sheet.getRow(inx).getCell(0).setCellValue((String) data.get("name"));

                Double value = Double.parseDouble(data.get("value").toString());
                sheet.getRow(inx).getCell(1).setCellValue(value.doubleValue());

                inx++;
            }
        }
    }

    /**
     * 根据word模板导出 针对图表（柱状图，折线图，饼图等）的处理
     *
     * @param docChart    图表对象
     * @param title       图表标题
     * @param seriesNames 系列名称数组
     * @param sheet       图形对应的sheet表
     * @param rowNum      总行数
     * @return
     */
    private XWPFChart wordExportChar(XWPFChart docChart, String title, String[] seriesNames, XSSFSheet sheet, int rowNum) {
        //获取图表数据对象
        XDDFChartData chartData = docChart.getChartSeries().get(0);

        //word图表均对应一个内置的excel，用于保存图表对应的数据
        //excel中 第一列第二行开始的数据为分类信息
        //CellRangeAddress(1, categories.size(), 0, 0) 四个参数依次为 起始行 截止行 起始列 截止列。
        //根据分类信息的范围创建分类信息的数据源
        XDDFDataSource catDataSource = XDDFDataSourcesFactory.fromStringCellRange(sheet, new CellRangeAddress(1, rowNum, 0, 0));
        //更新数据
        for (int i = 0; i < seriesNames.length; i++) {
            //excel中各系列对应的数据的范围
            //根据数据的范围创建值的数据源
            XDDFNumericalDataSource<Double> valDataSource = XDDFDataSourcesFactory.fromNumericCellRange(sheet, new CellRangeAddress(1, rowNum, 1, 1));
            //获取图表系列的数据对象
            XDDFChartData.Series series = chartData.getSeries().get(i);
            //替换系列数据对象中的分类和值
            series.replaceData(catDataSource, valDataSource);
            //修改系列数据对象中的标题
            CellReference cellReference = docChart.setSheetTitle(seriesNames[i], 1);
            series.setTitle(seriesNames[i], cellReference);
        }
        //更新图表数据对象
        docChart.plot(chartData);
        //图表整体的标题 传空值则不替换标题
        if (!Strings.isNullOrEmpty(title)) {
            docChart.setTitleText(title);
            docChart.setTitleOverlay(false);
        }

        return docChart;
    }

    /**
     * 持久化DB
     *
     * @param jobReportDTO
     */
    private void save2Db(JobReportDTO jobReportDTO) {
        if (jobReportDTO != null) {
            jobReportDTO.setId(ComUtil.getId());

            // 先删除
            this.deleteJobReport(jobReportDTO.getJobId(), Arrays.asList(jobReportDTO.getProcessInstanceId()));

            // 持久化
            this.insert(jobReportDTO);

            // 审计日志
            BdpLogUtil.log4Insert(jobReportDTO);
        }
    }

    /**
     * 从DB中获取质量报告
     *
     * @param jobId
     * @param processInstanceId
     * @return
     */
    private JobReportDTO queryJobReportFromDb(String jobId, String processInstanceId) {
        return this.mapper.getJobReportDetail(jobId, processInstanceId);
    }

    /**
     * 从redis获取生成报告Command
     *
     * @return
     */
    private JobReportCommandDTO getJobReportCmdFromRedis() {
        String jsonData = redisUtils.rpop(QaConstant.JOB_REPORT_REDIS_KEY);
        if (jsonData == null) {
            return null;
        }

        return JsonHelper.getInstance().readValue(
                jsonData,
                new SimpleDateFormat(Constants.YYYY_MM_DD_HH_MM_SS),
                new TypeReference<JobReportCommandDTO>() {
                });
    }

    /**
     * 命令转为作业
     *
     * @param command
     * @return
     */
    private JobDTO command2Job(JobReportCommandDTO command) {
        JobDTO job = new JobDTO();

        job.setId(command.getJobId());
        job.setName(command.getJobName());
        job.setProcessInstanceId(command.getProcessInstanceId());
        job.setMeasureType(command.getMeasureType());
        job.setState(command.getState());

        return job;
    }

    /**
     * 作业转为命令
     *
     * @param job
     * @return
     */
    private JobReportCommandDTO job2Command(JobDTO job) {
        JobReportCommandDTO command = new JobReportCommandDTO();

        command.setId(ComUtil.getId());
        command.setJobName(job.getName());
        command.setJobId(job.getId());
        command.setProcessInstanceId(job.getProcessInstanceId());
        command.setMeasureType(job.getMeasureType());
        command.setState(job.getState());

        return command;
    }

    /**
     * 构建指标查询条件
     * 注意：
     * 1、当前仅支持24h查询条件
     *
     * @return
     */
    private List<MetricDTO> buildMetricQeuryCond(String endTime) {
        List<MetricDTO> condList = new ArrayList<>();

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date currDate = null;
        try {
            currDate = sdf.parse(endTime);
        } catch (Exception e) {
            logger.error("查询指标失败，errMsg：", e);
        }

        Date startTime = null;
        for (int i = 23; i >= 0; i--) {
            startTime = new Date(currDate.getTime() - HOUR_MILLS);

            MetricDTO cond = new MetricDTO();
            cond.setStartTime(startTime);
            cond.setEndTime(currDate);
            condList.add(cond);

            currDate = startTime;
        }

        return condList;
    }

    /**
     * 填充任务信息-文件校验任务质量报告（自动执行）
     *
     * @param jobDTO
     * @param execTime
     * @return
     */
    private JobReportDTO fillAutoExecFileJobInfo(JobDTO jobDTO, String execTime) {
        Map<String, Object> jobInfoMap = Maps.newHashMap();

        // 任务名称
        jobInfoMap.put(QaConstant.JOB_NAME, jobDTO.getName() == null ? QaConstant.DOUBLE_HYPHEN : jobDTO.getName());
        // 校验类型
        jobInfoMap.put(QaConstant.MEASURE_TYPE_NAME, jobDTO.getMeasureType() == null ? QaConstant.DOUBLE_HYPHEN : MeasureType.getDescByCode(jobDTO.getMeasureType()));
        // 调度类型
        jobInfoMap.put(QaConstant.SCHEDULER_TYPE_NAME, jobDTO.getSchedulerType() == null ? QaConstant.DOUBLE_HYPHEN : SchedulerType.getDescByCode(jobDTO.getSchedulerType()));
        // 任务执行时间
        jobInfoMap.put(QaConstant.EXECUTE_TIME, execTime);
        // 类别
        jobInfoMap.put(QaConstant.TYPE_NAME, jobDTO.getTypeName() == null ? QaConstant.DOUBLE_HYPHEN : jobDTO.getTypeName());
        // 型号
        jobInfoMap.put(QaConstant.MODEL_CODE, jobDTO.getModelCode() == null ? QaConstant.DOUBLE_HYPHEN : jobDTO.getModelCode());
        // 机号
        jobInfoMap.put(QaConstant.AIRCRAFT_NAME, jobDTO.getAircraftName() == null ? QaConstant.DOUBLE_HYPHEN : jobDTO.getAircraftName());
        //飞行日期
        jobInfoMap.put(QaConstant.FLIGHT_DATE, jobDTO.getFlightDate() == null ? QaConstant.DOUBLE_HYPHEN : jobDTO.getFlightDate());
        // 架次
        jobInfoMap.put(QaConstant.SORTIE, jobDTO.getSortie() == null ? QaConstant.DOUBLE_HYPHEN : jobDTO.getSortie());
        // 测试系统
        jobInfoMap.put(QaConstant.TEST_SYSTEM_NAME, jobDTO.getTestSystemName() == null ? QaConstant.DOUBLE_HYPHEN : jobDTO.getTestSystemName());

        Map<String, Object> paraMap = new HashMap<>();
        paraMap.put(QaConstant.REPORT_JOB_INFO, jobInfoMap);

        JobReportDTO reportDTO = new JobReportDTO();
        reportDTO.setJobId(jobDTO.getId());
        reportDTO.setJobName(jobDTO.getId());
        reportDTO.setProcessInstanceId(jobDTO.getId());
        reportDTO.setReportMap(paraMap);

        return reportDTO;
    }

    /**
     * 填充质量报告文件信息-文件校验任务质量报告（自动执行）
     *
     * @param jobReportDTO
     * @param cntRule
     */
    private void fillAutoExecScoreInfo(JobReportDTO jobReportDTO, int cntRule) {
        Map<String, Object> scoreInfoMap = Maps.newHashMap();
        scoreInfoMap.put(QaConstant.SCORE, 100);
        scoreInfoMap.put(QaConstant.RULE_NUM, cntRule);
        scoreInfoMap.put(QaConstant.ALERT_NUM, 0);
        scoreInfoMap.put(QaConstant.RESULT, QaConstant.JOB_NORAML);

        Map<String, Object> paraMap = jobReportDTO.getReportMap();
        paraMap.put(QaConstant.REPORT_SCORE_INFO, scoreInfoMap);
    }

    /**
     * 填充质量报告文件信息-文件校验任务质量报告（自动执行）
     *
     * @param jobReportDTO
     * @param fileList
     */
    private void fillAutoExecFileInfo(JobReportDTO jobReportDTO, List<FtdpFileDTO> fileList) {
        int cnt = 1;
        List<List<Object>> fileInfoList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(fileList)) {
            for (FtdpFileDTO file : fileList) {
                List<Object> valueList = new ArrayList<>();

                valueList.add(cnt++);
                valueList.add(file.getName());
                valueList.add(file.getSize());
                valueList.add(file.getCreationDate());

                fileInfoList.add(valueList);
            }
        }

        Map<String, Object> paraMap = jobReportDTO.getReportMap();
        paraMap.put(QaConstant.REPORT_FILE_INFO, fileInfoList);
    }

    /**
     * 填充规则详情-文件校验任务质量报告（自动执行）
     *
     * @param jobDTO
     * @param ruleList
     * @param fileList
     * @param jobReportDTO
     */
    private void fillAutoExecRuleInfo(JobDTO jobDTO, List<RuleDTO> ruleList, List<FtdpFileDTO> fileList, JobReportDTO jobReportDTO) {
        Map<String, Object> paraMap = jobReportDTO.getReportMap();

        int seqNum = 1;
        List<Object> ruleDetailList = Lists.newArrayList();
        for (RuleDTO rule : ruleList) {
            List<Object> resultList = Lists.newArrayList();

            // 序号
            resultList.add(seqNum++);
            // 规则名称
            resultList.add(rule.getName());
            // 模板名称
            resultList.add(rule.getTemplateName() == null ? QaConstant.DOUBLE_HYPHEN : rule.getTemplateName());
            // 模板来源
            resultList.add(rule.getTemplateType() == null ? QaConstant.DOUBLE_HYPHEN : TemplateType.getDescByCode(rule.getTemplateType()));
            // 权重
            resultList.add(rule.getWeight() == null ? 0 : rule.getWeight());
            // 校验成功文件
            resultList.add(getFileNames(fileList) == null ? QaConstant.DOUBLE_HYPHEN : getFileNames(fileList));
            // 校验失败文件
            resultList.add(QaConstant.DOUBLE_HYPHEN);
            // 得分
            resultList.add(100);
            // 状态
            resultList.add(QaConstant.JOB_NORAML);

            ruleDetailList.add(resultList);
        }

        jobReportDTO.setScore(100.0);
        paraMap.put(QaConstant.RULE_DETAIL_INFO, ruleDetailList);
        jobReportDTO.setReportJson(JSONObject.toJSONString(paraMap));
    }

    /**
     * 自动执行任务
     *
     * @param schedulerType
     * @return
     */
    private boolean isAutoExecJob(Integer schedulerType) {
        return SchedulerType.AUTO.getCode() == schedulerType;
    }

    /**
     * 获取校验文件列表，多个文件使用英文逗号隔开
     *
     * @param fileList
     * @return
     */
    private String getFileNames(List<FtdpFileDTO> fileList) {
        if (CollectionUtils.isNotEmpty(fileList)) {
            List<String> nameList = new ArrayList<>();
            for (FtdpFileDTO file : fileList) {
                nameList.add(file.getName());
            }

            return String.join(QaConstant.COMMA, nameList);
        }

        return null;
    }

    /**
     * 发送订阅消息
     *
     * @param jobDTO
     * @param jobReport
     * @return
     */
    @SuppressWarnings("当前仅发送任务执行结果，后续会根据质量报警情况发送，2023-06-27，modified by wm")
    private boolean sendSysMsg(JobDTO jobDTO, JobReportDTO jobReport) {
        logger.info("当前任务：" + JSONObject.toJSONString(jobDTO));
        List<SubscribeDTO> subscribeList = this.subscribeService.getSubscribeList(jobDTO.getId());
        if (CollectionUtils.isEmpty(subscribeList)) {
            logger.warn("未找到订阅信息，质量任务【{}】不需要推送系统消息", jobDTO.getName());
            return true;
        }
        if (jobReport == null || jobReport.getJobResultType() == null) {
            logger.warn("质量任务【{}】未找到告警结果，不需要推送系统消息" + jobDTO.getName());
            return true;
        }
        if (jobReport.getJobResultType() != JobResultType.ALERT.getCode()) {
            logger.warn("质量任务【{}】校验结果为:{}，不需要推送系统消息", jobDTO.getName(), JobResultType.getDescByCode(jobReport.getJobResultType()));
            return true;
        }

        for (SubscribeDTO subscribe : subscribeList) {
            if (subscribe == null) {
                continue;
            }
            if (StringUtils.isEmpty(subscribe.getUserId())) {
                logger.warn("订阅用户ID为空.");
                continue;
            }

            SysMsgDTO msg = new SysMsgDTO();
            msg.setTitle("质量作业【" + jobDTO.getName() + "】告警");
            msg.setContent(new StringBuffer().append("质量作业名称：" + jobDTO.getName() + "\n").append("作业状态：告警" + "\n").toString());
            msg.setSendUser(ThreadContextHelper.getUserId() == null ? "1" : ThreadContextHelper.getUserId());
            msg.setRecvUser(subscribe.getUserId());
            msg.setSourceCode("personal");
            msg.setSourceName("数据质量通知");
            msg.setSendDate(new Date());
            msg.setMsgType("0");
            msg.setSendType("0");
            msg.setSysApplicationId(ThreadContextHelper.getAppId());

            logger.info("待发送系统消息：" + JSONObject.toJSONString(msg));
            this.sysMsgClient.sendmsg(msg);
        }

        return true;
    }
}
